package it.softphone.cmc.server.security;

import it.softphone.cmc.shared.entity.Account;
import it.softphone.cmc.shared.entity.TenantUser;
import it.softphone.cmc.shared.security.Roles;

import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAccount;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.crypto.RandomNumberGenerator;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.SimpleByteSource;

/**
 * 
 * @author bsorrentino
 *
 */
public abstract class AbstractMultiTenantRealm<USER extends Account> extends AuthorizingRealm {
	
	private final boolean _CACHE_ACCOUNT = true;
	
	protected static final int HASH_ITERATIONS = 1;
    protected static final String HASH_ALGORITHM = Sha256Hash.ALGORITHM_NAME;

    protected <T> void checkNotNull( T object, String message ) {
    	if( object == null ) throw new NullPointerException(message);
    }
    
    public AbstractMultiTenantRealm() {
		super();
		setCredentialsMatcher(theCredentialMatcher());
	}

	public AbstractMultiTenantRealm(CacheManager cacheManager) {
		this();
		setCacheManager(cacheManager);
	}

	@Override
	public final  boolean supports(AuthenticationToken token) {
		return (token instanceof MultiTenantAuthenticationToken) || (token instanceof UsernamePasswordToken);
	}

	private AuthenticationInfo doGetAuthenticationInfo(String userName, String tenant) throws AuthenticationException {
        checkNotNull(userName, "User name can't be null");

        getLogger().fine("Finding authentication info for " + userName + " in DB");
        USER user = findUser(userName, tenant );

        if( user == null ) {
        	getLogger().warning( String.format("User [%s] cannot be found!", userName));
        	return null;
        }
        if (userIsNotQualified(user)) {
        	getLogger().warning( String.format("User [%s] is not qualified!", userName));
            return null;
        }
        getLogger().info("Found " + userName + " in DB");
        
        SimplePrincipalCollection c = new SimplePrincipalCollection();
		
        c.add( getUserName(user), getName());
	
if( _CACHE_ACCOUNT  ) {
		c.add( user, getName() );	
}

		SimpleAccount account = new SimpleAccount(c, getUserPasswordHash(user), new SimpleByteSource(getUserSalt(user)), getName());
        account.setRoles( getUserRoles(user));
        account.setStringPermissions(getUserPermissions(user));
        
        return account;
    }

    @Override
    protected final AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        final String userName = ((UsernamePasswordToken) token).getUsername();
        
        final String tenant = ( token instanceof MultiTenantAuthenticationToken ) ?
        					((MultiTenantAuthenticationToken) token).getTenant() :
        					"";	
        
        return doGetAuthenticationInfo(userName, tenant);
    }

    @SuppressWarnings("unchecked")
	@Override
    protected final AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        checkNotNull(principals, "You can't have a null collection of principals");
        if( principals.isEmpty() ) throw new IllegalArgumentException("principals is empty");
        
        //String userName = (String) getAvailablePrincipal(principals);

        USER user;
        
if( _CACHE_ACCOUNT  ) {
		user = (USER) principals.oneByType(Account.class); 	
}
else {
		java.util.Iterator<Object> i = principals.iterator();
        
        String userName = i.next().toString();
        
        if (userName == null) {
            throw new NullPointerException("Can't find a principal in the collection");
        }

        String tenant = null;
        if( i.hasNext() ) {
        	tenant = i.next().toString();
        
	        if (tenant == null) {
	            throw new NullPointerException("Can't find a tenant principal in the collection");
	        }
        }
        getLogger().fine( String.format("Finding authorization info for [%s] in tenant [%s]",userName, tenant));
        
        user = findUser(userName, tenant );
        if( user == null ) {
        	getLogger().warning( String.format("User [%s] cannot be found!", userName));
        	return null;
        }
        if (userIsNotQualified(user)) {
        	getLogger().warning( String.format("User [%s] is not qualified!", userName));
            return null;
        }
        getLogger().fine("Found " + userName + " in DB");
}   


        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(getUserRoles(user));
        
        Set<String> permissions = getUserPermissions(user);
        
        getLogger().info( "user permissions");
        for( String p : permissions ) {
        	getLogger().info( String.format("\tperm[%s]", p));
        }
        info.setStringPermissions( permissions );
        return info;
    }

    protected final  boolean userIsNotQualified(USER user) {
        return !isUserRegistered(user) || isUserSuspendend( user );
    }
    
    public static ByteSource salt() {
        RandomNumberGenerator rng = new SecureRandomNumberGenerator();
        return rng.nextBytes();
    }

    public static String hash(String password, byte[] salt) {
        return new Sha256Hash(password, new SimpleByteSource(salt), HASH_ITERATIONS).toHex();
    }
    
    public static CredentialsMatcher theCredentialMatcher() {
        HashedCredentialsMatcher credentials = new HashedCredentialsMatcher(HASH_ALGORITHM);
        credentials.setHashIterations(HASH_ITERATIONS);
        /**
         * storedCredentialsHexEncoded the indicator if this system's stored credential hash is Hex encoded or not ('not' automatically implying it is Base64 encoded).
         */
        credentials.setStoredCredentialsHexEncoded(true);
        return credentials;
    }

    ///
    ///
    ///
    protected abstract java.util.logging.Logger getLogger();
    protected abstract USER findUser( String name, String tenant );

    ///
    /// USER INFORMATION
    ///
	protected final String getUserName( USER user ) {
		return user.getId();
	}

	static final java.util.Set<String> GUEST_ROLES = Collections.unmodifiableSet( new java.util.HashSet<String>( Arrays.asList( Roles.B2B, Roles.GUEST) ) );

	protected final Set<String> getUserRoles( USER user ) {
		TenantUser tenantUser = user.getUser();
		
		if( tenantUser == null ) {
			
			getLogger().warning( String.format( "Tenant User nont found for account [%s]", user.getId()));
			
			
			return GUEST_ROLES;
		}
		
		return tenantUser.getRoles();
	}
	
	protected abstract String getUserPasswordHash( USER user );
	protected abstract byte[] getUserSalt( USER user);
    protected abstract boolean isUserRegistered( USER user );
    protected abstract boolean isUserSuspendend( USER user );
    protected abstract Set<String> getUserPermissions( USER user );
    

    
}
